#!/usr/bin/env python
# Copyright (C) 2010 Anthony LaTorre
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""A simple regex search [and replace]"""

import os
import sys
import optparse
import string
import re

usage = "usage: %prog <pattern> [directory ...] [file ...]"
parser = optparse.OptionParser(usage)
parser.add_option("-r", "--recursive", action="store_true", dest="recursive",
                  help="recurse into subdirectories", default=False)
parser.add_option("-a", "--all", action="store_true", dest="all",
                  help="do not ignore any files", default=False)
parser.add_option("-i", "--insensitive", action="store_true", dest="lazy",
                  help="case-insensitive search", default=False)
parser.add_option("-s", "--susbstitute", dest="sub",
                  help="substitute SUB for <pattern>", default=None)
parser.add_option("-f", "--force", action="store_true", dest="force",
                  help="force substitution", default=False)
parser.add_option("-b", "--backup", action="store_true", dest="backup",
                  help="backup any changed files", default=False)
parser.add_option("-n", "--no-colors", action="store_false", dest="colors",
                  help="turn off syntax highlighting", default=True)
(options, args) = parser.parse_args()

if len(args) < 1:
    parser.print_help()
    sys.exit(1)

if len(args) < 2:
    args.append(os.getcwd())

bad_prefix = ('.')
bad_suffix = ('.a', '.o', '.bak')

re_flags = options.lazy and re.IGNORECASE or 0

try:
    rex = re.compile(args[0], re_flags)
except re.error:
    print 'Error: %s is not a valid regular expression' % args[0]
    sys.exit(1)

class Colors:
    purple = options.colors and '\033[95m' or ''
    blue = options.colors and '\033[94m' or ''
    green = options.colors and '\033[92m' or ''
    normal = options.colors and '\033[0m' or ''

def ask_ok(prompt, default=True):
    """Asks the user a yes/no question."""
    while True:
        ok = raw_input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        if ok in (''):
            return default
        print 'Yes or no, please!'

def is_text(file):
    """Determines if a file contains printable characters."""
    try:
        f = open(file)
    except IOError:
        print 'Cannot open file', file
        return False
    buffer = f.read(500)
    f.close()

    for char in buffer:
        if not char in string.printable:
            return False
    return True

def print_header(file):
    """Pretty prints a filename header."""
    print '=' * 65
    print Colors.purple + file + Colors.normal
    print

def search_file(file, regex):
    """Prints lines in a file matching a regex."""
    try:
        f = open(file)
    except IOError:
        print 'Cannot open file', file
        return None
    instance = False
    for i, line in enumerate(f):
        if regex.search(line):
            if not instance:
                print_header(file)
                instance = True
            print '@'+str(i+1).zfill(4)+':',
            print regex.sub(Colors.blue+'\g<0>'+Colors.normal, line).strip()
    f.close()

def replace_file(file, regex, sub, force=False, backup=False):
    """Prints lines in a file matching a regex and prompts user to replace."""
    try:
        f = open(file)
    except IOError:
        print 'Cannot open file', file
        return None
    newlines = []
    instance = False
    changed = False
    for i, line in enumerate(f):
        newlines.append(line)
        if regex.search(line):
            if not instance:
                print_header(file)
                instance = True
            print '-',
            print regex.sub(Colors.blue+'\g<0>'+Colors.normal, line).strip()
            print '+',
            print regex.sub(Colors.green+sub+Colors.normal, line).strip()
            if force or ask_ok('Replace [y]/n?'):
                newlines[-1] = regex.sub(sub, line)
                changed = True
    f.close()

    if changed:
        if backup:
            os.rename(file, file+'.bak')
        new_file = open(file, 'w')
        for line in newlines:
            new_file.write(line)
        new_file.close()

def search(file, regex, sub=None, force=False, backup=False):
    if not sub==None:
        replace_file(file, regex, sub, force, backup)
    else:
        search_file(file, regex)

for path in args[1:]:
    if os.path.isfile(path):
        search(path, rex, options.sub, options.force, options.backup)
    elif os.path.isdir(path):
        for root, dirs, files in os.walk(path):
            # remove directories beginning with a period
            for dirname in dirs[:]:
                if dirname.startswith('.'):
                    dirs.remove(dirname)
            if not options.all:
                # remove files beginning with bad prefix/suffix
                for filename in files[:]:
                    for prefix in bad_prefix:
                        if filename.startswith(prefix):
                            files.remove(filename)
                            break
                    for suffix in bad_suffix:
                        if filename.endswith(suffix):
                            files.remove(filename)
                            break
            # remove non-text files
            for filename in files[:]:
                if not is_text(os.path.join(root, filename)):
                    files.remove(filename)
            # loop over remaining files in directory
            for file in [os.path.join(root, filename) for filename in files]:
                search(file, rex, options.sub, options.force, options.backup)
            # break from os.walk() after single iteration
            if not options.recursive:
                break
    else:
        print 'Error: %s is not a valid path' % path
        sys.exit(1)
